package org.groovyflow.util
/**
 * Emulates Scala pattern matching, but in addition allows Cases to rely on arguments that are not in closure scope.
 * @author Chuck Tassoni
 */
@Typed
class PatternMatch {
  //You can choose to pass cases into <code>match()</code> instead of adding cases to this member
   List<Case> cases = new ArrayList<Case>()

  //When using @Typed these become truly private, which is what I want
  private def argsForPred
  private def argsForAction

  private boolean okToThrowExceptionWhenNoMatch = true
  Closure transform = { returnedFromPredicate ->
    returnedFromPredicate instanceof List ? returnedFromPredicate :
      [returnedFromPredicate]
  }
  Closure makeFinalArgsForAction = {actionArgsSoFar, List argsReturnedsFromPred ->
    if (!(actionArgsSoFar instanceof List)) {
      def temp = []
      temp << actionArgsSoFar
      temp = temp +  argsReturnedsFromPred
      return temp
    } else {
      return ((List) actionArgsSoFar) + argsReturnedsFromPred
    }
  }

  public PatternMatch okToThrowExceptionWhenNoMatch(boolean b) {
    okToThrowExceptionWhenNoMatch = b
    this
  }
  /**
   * Arguments to be sent to case predicates and actions.  Note that you may choose to have your
   * Case predicates and actions access closure scoped variables only, in which case you won't need to use
   * this method.
   * @param predArgs the arguments (if you pass a list) or argument to be sent to each Case predicate.
   * @param actionArgs the arguments (if you pass a list) or argument to be sent to each Case action.  If you
   * don't provide actionArgs, we'll assume they're the same as the predicate args.
   * @return this , so that you can chain this method, as in
   * <code>new PatternMatch().args([1,2])</code>
   */
  public PatternMatch args(predArgs, actionArgs = null) {
    if (actionArgs == null) actionArgs = predArgs
    this.argsForPred = predArgs
    this.argsForAction = actionArgs
    this
  }

  /**
   * Runs through each case until we find a match.  You have the choice of supplying cases by
   * passing them as an argument to this method or by adding to our 'cases member variable., with the
   * former taking precedence (and the member cases not being used at all in this invocation).
   * <p>
   * We consider a case has been matched when the case's predicate closure returns something other than false or null.
   * The value returned by the predicate is then available to the action.  If the predicate returns a list,
   * the individual elements of the list are available as parameters of the action. For example,
   * when a predicate returns [x,y,z], the associated predicate might look like this:
   *{x,y,z ->  etc.} .  But note that the predicate can also decide not to declare all the variables,
   * and so {x,y -> etc.} would also e a valid declaration.
   * <p>
   * If a predicate only wishes to return a single scalar value, it needn't put that value in a list-- PatternMatch
   * will do the work.  If you've provided arguments via the <code>args</code> method before invoking this method,
   * then the value returned by the predicate is available to the action only after all the arguments passed into
   * args() have been declared.
   *
   * @param cases If this is null we'll use the member variable of the same name
   * @return the result of the pattern match
   */
  def match(Case... cases = null) {
    List theCases = cases == null ? this.cases : cases
    for (aCase in theCases) {
      def decisionBasis = ClosureHelp.invokeClosure(aCase.pred, argsForPred)
      if (decisionBasis == null || (decisionBasis instanceof Boolean && !decisionBasis) )
        continue;
      def transformed = transform(decisionBasis)
      if (argsForAction == null)
        return ClosureHelp.invokeClosure(aCase.action, transformed)
      else {
        return ClosureHelp.invokeClosure(aCase.action, makeFinalArgsForAction(argsForAction, transformed))
      }
    }
    if (okToThrowExceptionWhenNoMatch)
      throw new NoMatchException()
  }
}